package net.aiion.weave.impl.ui.xml;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import net.aiion.weave.asm.Label;
import net.aiion.weave.asm.Opcodes;
import net.aiion.weave.impl.ui.BindableFactory;
import net.aiion.weave.impl.ui.binder.BinderException;
import net.aiion.weave.spi.bytecode.BytecodeGenerator;
import net.aiion.weave.spi.cards.CardContext;
import net.aiion.weave.spi.cards.events.CardEvent;
import net.aiion.weave.spi.cards.expressions.CardExpressionResolverService;
import net.aiion.weave.spi.cards.expressions.CardExpressionTokenizerAction;
import net.aiion.weave.spi.cards.expressions.CardExpressionTokenizerService;
import net.aiion.weave.spi.injection.annotations.Inject;
import net.aiion.weave.spi.naming.Context;
import net.aiion.weave.spi.naming.Contextual;
import net.aiion.weave.spi.ui.Bindable;
import net.aiion.weave.spi.ui.Component;
import net.aiion.weave.spi.ui.Container;
import net.aiion.weave.spi.xml.XmlContext;
import net.aiion.weave.spi.xml.XmlQualifiedName;
import net.aiion.weave.spi.xml.XmlSink;
import net.aiion.weave.spi.xml.events.AttributeXmlEvent;
import net.aiion.weave.spi.xml.events.CharactersXmlEvent;
import net.aiion.weave.spi.xml.events.ElementEndXmlEvent;
import net.aiion.weave.spi.xml.events.ElementStartXmlEvent;
import net.aiion.weave.spi.xml.events.XmlEvent;

public class BindableTransformation implements XmlSink
{
    private static final String PROP_INTERNAL_NAME = "internalName";

    private static final int INDEX_CARD_CONTEXT = 1;
    private static final int INDEX_BINDABLE = 2;
    private static final int INDEX_STRING_BUILDER = 3;

    private static final String CONTAINERS_FIELD_NAME = "containers$$BindableXmlSink";

    private static final String ID_ATTRIBUTE_NAME = "id";

    @Inject("sourceClass")
    private Class<?> sourceClass;

    @Inject
    private BindableFactory bindableFactory;

    @Inject
    private CardExpressionTokenizerService cardExpressionTokenizerService;

    @Inject
    private BytecodeGenerator bytecodeGenerator;

    private final Set<String> bindableIds;
    private final Stack<Bindable> bindables;

    private final Map<String, Map<String, String>> referencedBindableIdsByBindableId;

    private String currentBindableId;
    private Bindable currentBindable;
    private boolean hasCharacters = false;

    public BindableTransformation() {
        bindableIds = new HashSet<>();
        bindables = new Stack<>();
        referencedBindableIdsByBindableId = new HashMap<>();
    }

    @Override
    public void handleXmlEvent(final XmlContext aXmlContext) {
        final XmlEvent xmlEvent = aXmlContext.getXmlEvent();
        switch (xmlEvent.getType()) {
        case CardEvent.CARD_CLASS_START:
            bytecodeGenerator.startField(Opcodes.ACC_PRIVATE, CONTAINERS_FIELD_NAME, Stack.class);
            bytecodeGenerator.endField();
            break;
        case CardEvent.CARD_PROCESS_CARD_METHOD_START:
            bytecodeGenerator.newReference(Stack.class);
            bytecodeGenerator.duplicateReference();
            bytecodeGenerator.invokeConstructor(Stack.class);
            bytecodeGenerator.storeField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
                CONTAINERS_FIELD_NAME, Stack.class);
            break;
        case CardEvent.CARD_PROCESS_CARD_METHOD_END:
            break;
        case CardEvent.CARD_CLASS_END:
            break;
        case XmlEvent.START_ELEMENT:
            handleElementStartXmlEvent(aXmlContext, xmlEvent.asElementStart());
            break;
        case XmlEvent.ATTRIBUTE:
            handleAttributeXmlEvent(aXmlContext, xmlEvent.asAttribute());
            break;
        case XmlEvent.CHARACTERS:
            handleCharactersXmlEvent(aXmlContext, xmlEvent.asCharacters());
            break;
        case XmlEvent.END_ELEMENT:
            handleElementEndXmlEvent(aXmlContext, xmlEvent.asElementEnd());
            break;
        default:
            aXmlContext.forwardXmlEvent(xmlEvent);
            break;
        }
    }

    private void handleElementStartXmlEvent(final XmlContext aXmlContext,
        final ElementStartXmlEvent aElementStartXmlEvent)
    {
        final XmlQualifiedName qualifiedName = aElementStartXmlEvent.getName();

        if (hasCharacters) {
            throw new BinderException(String.format(
                "Component of type '%s' must either declare child components or characters content, but not both",
                currentBindable.getClass().getName()));
        }

        if (!bindables.isEmpty() && !(currentBindable instanceof Container)) {
            throw new BinderException(String.format("Component of type '%s' does not accept any child components.",
                currentBindable.getClass().getName()));
        }

        currentBindable = bindableFactory.createBindable(qualifiedName.getNamespaceURI(), qualifiedName.getLocalName());

        currentBindableId = createBindableId(aElementStartXmlEvent.getAttributeValue(ID_ATTRIBUTE_NAME));
        bindableIds.add(currentBindableId);

        // Generates:
        // <<INDEX_BINDABLE>> = <<INDEX_CARD_CONTEXT>>.getContext().lookup(<<currentBindableId>>);
        // if (<<INDEX_BINDABLE>> == null) {
        final Label bindableInitializedLabel = new Label();

        bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
        bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
        bytecodeGenerator.pushConstant(currentBindableId);
        bytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, String.class);
        bytecodeGenerator.storeReference(INDEX_BINDABLE);
        bytecodeGenerator.loadReference(INDEX_BINDABLE);
        bytecodeGenerator.jumpNotEquals(bindableInitializedLabel);

        // Generates:
        // <<INDEX_BINDABLE>> = <<INDEX_CARD_CONTEXT>>.getContext().lookup(BindableFactory.class)
        // .createBindable(<<INDEX_CARD_CONTEXT>>.getContext(), <<qualifiedName.getNamespaceURI()>>,
        // <<qualifiedName.getLocalName()>>);
        bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
        bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
        bytecodeGenerator.pushConstant(BindableFactory.class);
        bytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, Class.class);
        bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
        bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
        bytecodeGenerator.pushConstant(qualifiedName.getNamespaceURI());
        bytecodeGenerator.pushConstant(qualifiedName.getLocalName());
        bytecodeGenerator.invokeVirtual(BindableFactory.class, "createBindable", Bindable.class, Context.class,
            String.class, String.class);
        bytecodeGenerator.storeReference(INDEX_BINDABLE);

        // Generates:
        // }
        bytecodeGenerator.markLabel(bindableInitializedLabel);
        bytecodeGenerator.markFrameSame();

        if (!bindables.isEmpty() && currentBindable instanceof Component) {
            // Generates: ((Container)this.containers$.peek()).addComponent((Component) <<INDEX_BINDABLE>>);
            bytecodeGenerator.loadReference(0);
            bytecodeGenerator.loadField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
                CONTAINERS_FIELD_NAME, Stack.class);
            bytecodeGenerator.invokeVirtual(Stack.class, "peek", Object.class);
            bytecodeGenerator.castChecked(Container.class);
            bytecodeGenerator.loadReference(INDEX_BINDABLE);
            bytecodeGenerator.castChecked(Component.class);
            bytecodeGenerator.invokeInterface(Container.class, "addComponent", void.class, Component.class);
        }

        if (currentBindable instanceof Container) {
            // Generates: this.containers$.add(<<INDEX_BINDABLE>>);
            bytecodeGenerator.loadReference(0);
            bytecodeGenerator.loadField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
                CONTAINERS_FIELD_NAME, Stack.class);
            bytecodeGenerator.loadReference(INDEX_BINDABLE);
            bytecodeGenerator.invokeVirtual(Stack.class, "add", boolean.class, Object.class);
        }

        bindables.add(currentBindable);

        final Label skipBindingLabel = new Label();

        // Generates: if (<<INDEX_CARD_CONTEXT>>.getContext().lookup(<<currentBindableId>>) == null) {
        bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
        bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
        bytecodeGenerator.pushConstant(currentBindableId);
        bytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, String.class);
        bytecodeGenerator.jumpNotEquals(skipBindingLabel);

        // Generates: <<INDEX_CARD_CONTEXT>>.getContext().bind(<<currentBindableId>>, <<INDEX_BINDABLE>>);
        bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
        bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
        bytecodeGenerator.pushConstant(currentBindableId);
        bytecodeGenerator.loadReference(INDEX_BINDABLE);
        bytecodeGenerator.invokeInterface(Context.class, "bind", Contextual.class, String.class, Object.class);

        // Generates: }
        bytecodeGenerator.markLabel(skipBindingLabel);
        bytecodeGenerator.markFrameSame();

        if (referencedBindableIdsByBindableId.containsKey(currentBindableId)) {
            final Map<String, String> referencedBindableIds = referencedBindableIdsByBindableId
                .remove(currentBindableId);
            for (String waitingId : referencedBindableIds.keySet()) {
                // Generates: ((Bindable) <<INDEX_CARD_CONTEXT>>.getContext().lookup(<<waitingId>>))
                // .bindValue(<<referencedBindableIds.get(waitingId)>>, <<INDEX_BINDABLE>>);
                bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
                bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
                bytecodeGenerator.pushConstant(waitingId);
                bytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, String.class);
                bytecodeGenerator.castChecked(Bindable.class);
                bytecodeGenerator.pushConstant(referencedBindableIds.get(waitingId));
                bytecodeGenerator.loadReference(INDEX_BINDABLE);
                bytecodeGenerator.invokeInterface(Bindable.class, "bindValue", void.class, String.class, Object.class);
            }
        }

        for (AttributeXmlEvent attributeXmlEvent : aElementStartXmlEvent.getAttributes()) {
            handleAttributeXmlEvent(aXmlContext, attributeXmlEvent);
        }
    }

    private String createBindableId(final String aId) {
        final StringBuilder stringBuilder = new StringBuilder(sourceClass.getName());
        stringBuilder.append('/');

        if (aId != null) {
            stringBuilder.append(aId);
            return stringBuilder.toString();
        }

        if (!bindableIds.isEmpty()) {
            stringBuilder.append(String.valueOf(bindableIds.size()));
        }

        return stringBuilder.toString();
    }

    private void handleAttributeXmlEvent(final XmlContext aXmlContext, final AttributeXmlEvent aAttributeXmlEvent) {
        final String attributeName = aAttributeXmlEvent.getLocalName();
        final String attributeValue = aAttributeXmlEvent.getValue();

        if (ID_ATTRIBUTE_NAME.equals(attributeName)) {
            return;
        }

        if (!Component.class.isAssignableFrom(currentBindable.getValueType(attributeName))) {
            // Generates: <<INDEX_STRING_BUILDER>> = new StringBuilder();
            bytecodeGenerator.newReference(StringBuilder.class);
            bytecodeGenerator.duplicateReference();
            bytecodeGenerator.invokeConstructor(StringBuilder.class);
            bytecodeGenerator.storeReference(INDEX_STRING_BUILDER);

            cardExpressionTokenizerService.tokenizeExpressions(attributeValue, new CardExpressionTokenizerAction()
            {
                @Override
                public void handleTextToken(final String aTextToken) {
                    // Generates: <<INDEX_STRING_BUILDER>>.append(<<aTextToken>>);
                    bytecodeGenerator.loadReference(INDEX_STRING_BUILDER);
                    bytecodeGenerator.pushConstant(aTextToken);
                    bytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
                    bytecodeGenerator.popReference();
                }

                @Override
                public void handleExpressionToken(final String aExpressionPrefixToken,
                    final String aExpressionValueToken)
                {
                    // Generates:
                    // <<INDEX_STRING_BUILDER>>.append(String.valueOf(<<INDEX_CARD_CONTEXT>>.getExpressionResolverService()
                    // .readExpression(<<aExpressionPrefixToken>>, <<aExpressionValueToken>>)));
                    bytecodeGenerator.loadReference(INDEX_STRING_BUILDER);
                    bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
                    bytecodeGenerator.invokeInterface(CardContext.class, "getExpressionResolverService",
                        CardExpressionResolverService.class);
                    bytecodeGenerator.pushConstant(aExpressionPrefixToken);
                    bytecodeGenerator.pushConstant(aExpressionValueToken);
                    bytecodeGenerator.invokeInterface(CardExpressionResolverService.class, "readExpression",
                        Object.class, String.class, String.class);
                    bytecodeGenerator.invokeStatic(String.class, "valueOf", String.class, Object.class);
                    bytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
                    bytecodeGenerator.popReference();
                }
            });

            // Generates: <<INDEX_BINDABLE>>.bindValue(<<attributeName>>, <<INDEX_STRING_BUILDER>>.toString());
            bytecodeGenerator.loadReference(INDEX_BINDABLE);
            bytecodeGenerator.loadReference(INDEX_STRING_BUILDER);
            bytecodeGenerator.pushConstant(attributeName);
            bytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);
            bytecodeGenerator.invokeInterface(Bindable.class, "bindValue", void.class, String.class, Object.class);
            return;
        }

        final String bindableId = createBindableId(attributeValue);
        if (bindableIds.contains(attributeValue)) {
            // Generates: <<INDEX_BINDABLE>>.bindValue(<<attributeName>>,
            // <<INDEX_CARD_CONTEXT>>.getContext().lookup(<<bindableId>>));
            bytecodeGenerator.loadReference(INDEX_BINDABLE);
            bytecodeGenerator.pushConstant(attributeName);
            bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
            bytecodeGenerator.invokeInterface(CardContext.class, "getContext", Context.class);
            bytecodeGenerator.pushConstant(bindableId);
            bytecodeGenerator.invokeInterface(Context.class, "lookup", Object.class, String.class);
            bytecodeGenerator.invokeInterface(Bindable.class, "bindValue", void.class, String.class, Object.class);
        } else {
            if (!referencedBindableIdsByBindableId.containsKey(bindableId)) {
                referencedBindableIdsByBindableId.put(bindableId, new HashMap<String, String>());
            }
            referencedBindableIdsByBindableId.get(bindableId).put(currentBindableId, attributeName);
        }
    }

    private void handleCharactersXmlEvent(final XmlContext aXmlContext, final CharactersXmlEvent aCharactersXmlEvent) {
        if (!hasCharacters) {
            hasCharacters = true;

            // Generates: <<INDEX_CONTENT>> = new StringBuilder();
            bytecodeGenerator.newReference(StringBuilder.class);
            bytecodeGenerator.duplicateReference();
            bytecodeGenerator.invokeConstructor(StringBuilder.class);
            bytecodeGenerator.storeReference(INDEX_STRING_BUILDER);
        }

        cardExpressionTokenizerService.tokenizeExpressions(aCharactersXmlEvent.getData(),
            new CardExpressionTokenizerAction()
            {
                @Override
                public void handleTextToken(final String aTextToken) {
                    // Generates:: <<INDEX_CONTENT>>.append(<<aTextToken>>);
                    bytecodeGenerator.loadReference(INDEX_STRING_BUILDER);
                    bytecodeGenerator.pushConstant(aTextToken);
                    bytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
                    bytecodeGenerator.popReference();
                }

                @Override
                public void handleExpressionToken(final String aExpressionPrefixToken,
                    final String aExpressionValueToken)
                {
                    // Generates:
                    // <<INDEX_CONTENT>>.append(String.valueOf(<<INDEX_CARD_CONTEXT>>.getExpressionResolverService()
                    // .readExpression(<<aExpressionPrefixToken>>, <<aExpressionValueToken>>));
                    bytecodeGenerator.loadReference(INDEX_STRING_BUILDER);
                    bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
                    bytecodeGenerator.invokeInterface(CardContext.class, "getExpressionResolverService",
                        CardExpressionResolverService.class);
                    bytecodeGenerator.pushConstant(aExpressionPrefixToken);
                    bytecodeGenerator.pushConstant(aExpressionValueToken);
                    bytecodeGenerator.invokeInterface(CardExpressionResolverService.class, "readExpression",
                        Object.class, String.class, String.class);
                    bytecodeGenerator.invokeStatic(String.class, "valueOf", String.class, Object.class);
                    bytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
                    bytecodeGenerator.popReference();
                }
            });
    }

    private void handleElementEndXmlEvent(final XmlContext aXmlContext, final ElementEndXmlEvent aElementEndXmlEvent) {
        if (hasCharacters) {
            // Generates: <<INDEX_BINDABLE>>.bindValue(<<INDEX_CONTENT>>.toString());
            bytecodeGenerator.loadReference(INDEX_BINDABLE);
            bytecodeGenerator.loadReference(INDEX_STRING_BUILDER);
            bytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);
            bytecodeGenerator.invokeInterface(Bindable.class, "bindValue", Object.class);
            hasCharacters = false;
        }

        if (bindables.pop() instanceof Container) {
            // Generates: this.containers$.pop();
            bytecodeGenerator.loadReference(0);
            bytecodeGenerator.loadField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
                CONTAINERS_FIELD_NAME, Stack.class);
            bytecodeGenerator.invokeVirtual(Stack.class, "pop", Object.class);
            bytecodeGenerator.popReference();
        }
    }
}
